local _G = _G
local UnitExists = _G.UnitExists
local UnitName = _G.UnitName
local UnitBuff = _G.UnitBuff
local UnitDebuff = _G.UnitDebuff
local GetNumRaidMembers = _G.GetNumRaidMembers
local GetNumPartyMembers = _G.GetNumPartyMembers

local BAR_TEXT = "%d / %3.1f%% / %.1fs"
local CHAT_TEXT = "Uptime for %s on %s : %d gains over %.1f seconds. %3.1f%% uptime"

UptimeDB = {}

-- @FIXME for wotlk, replace GetAura() hack with UnitAura
local function IsUnit(unitid, name)
	if unitid == name then return unitid end
	local n = UnitName(unitid)
	if n == name then return unitid end
end

local function GetUnitId(name, previous_id, previous_targetting)
	if previous_id and IsUnit(previous_id, name) then
		return previous_id, previous_targetting
	end
	local unit = IsUnit("player", name)
	if unit then return unit end
	local nr, np = GetNumRaidMembers()
	if nr == 0 then
		np = GetNumPartyMembers()
		for i = 1, np do
			local unit = IsUnit("party"..i, name)
			if unit then return unit end
		end
	else
		for i = 1, nr do
			local unit = IsUnit("raid"..i, name)
			if unit then return unit end
		end
	end
	local unit = IsUnit("target", name) or IsUnit("focus", name)
	if unit then return unit, "player" end
	if nr == 0 then
		for i = 1, np do
			local unit = "party"..i
			local target = IsUnit(unit.."target", name)
			if target then return target, unit end
		end
	else
		for i = 1, nr do
			local unit = "raid"..i
			local target = IsUnit(unit.."target", name)
			if target then return target, unit end
		end
	end
end

local function UnitHasAura(unit, name, previous_id, GetAura)
	local n, _, app

	if previous_id then
		n, _, _, app = GetAura(unit, previous_id)
		if n == name then
			return true, previous_id, app
		end
	end
	local i = 1
	while true do
		n, _, _, app = GetAura(unit, i)
		if not n then
			return nil, nil, ""
		elseif name == n then
			return true, i, app
		else
			i = i + 1
		end
	end
end

local function Frame_OnUpdateTarget(self, elapsed)
	local total = self.elapsed + elapsed
	local with_aura = self.elapsed_with
	local has_aura, applications
	local unit = self.unit
	if not unit then return end
	has_aura, self.previous_aura_id, applications = UnitHasAura(unit, self.aura_name, self.previous_aura_id, self.getaura)
	self.has_aura = has_aura
	if has_aura then
		with_aura = with_aura + elapsed
		self.elapsed_with = with_aura
	end
	self.elapsed = total
	local value
	if total > 0 then
		value = with_aura / total * 100
	else
		value = 0
	end
	self.sb:SetValue(value)
	self.text:SetFormattedText(BAR_TEXT, self.gain, value, total)
	self.app:SetText(applications ~= 0 and applications or "")
end

local function Frame_OnUpdate(self, elapsed)
	local total = self.elapsed + elapsed
	local with_aura = self.elapsed_with
	if self.has_aura then
		with_aura = with_aura + elapsed
		self.elapsed_with = with_aura
	end
	self.elapsed = total
	local value
	if total > 0 then
		value = with_aura / total * 100
	else
		value = 0
	end
	self.sb:SetValue(value)
	self.text:SetFormattedText(BAR_TEXT, self.gain, value, total)
end

local ReorderUpTimeFrames, SetFrameInfo

local function SetRegisteredTarget(frame, set)
	if set then
		if not frame.registered_target then
			frame:RegisterEvent("UNIT_TARGET")
			frame:RegisterEvent("PLAYER_TARGET_CHANGED")
			frame:RegisterEvent("PLAYER_FOCUS_CHANGED")
			frame.registered_target = true
			frame.OnUpdate = Frame_OnUpdateTarget
		end
	elseif frame.registered_target then
		frame:UnregisterEvent("UNIT_TARGET")
		frame:UnregisterEvent("PLAYER_TARGET_CHANGED")
		frame:UnregisterEvent("PLAYER_FOCUS_CHANGED")
		frame.OnUpdate = Frame_OnUpdate
		frame.registered_target = nil
	end
end

local events = {}

local function UpdateAuraInfo(self)
	local had_buff, has_aura, aura_id, applications = self.has_aura
	has_aura, self.previous_aura_id, applications = UnitHasAura(self.unit, self.aura_name, self.previous_aura_id, self.getaura)
	self.has_aura = has_aura
	if self.recording then
		if not had_buff and has_aura then
			self.gain = self.gain + 1
			self.sb:SetStatusBarColor(0.1, 0.9, 0.1)
		elseif had_buff and not has_aura then
			self.sb:SetStatusBarColor(0.9, 0.1, 0.1)
		end
		self.app:SetText(applications ~= 0 and applications or "")
	end
end

function events:RAID_ROSTER_UPDATE()
	local unit, targetting = GetUnitId(self.char_name, self.unit, self.targetting)
	self.unit = unit
	self.targetting = targetting
	if unit then
		self.cname:SetTextColor(1, 1, 1)
		UpdateAuraInfo(self)
		SetRegisteredTarget(self, targetting)
	else
		self.has_aura = nil
		self.previous_aura_id = nil
		SetRegisteredTarget(self, false)
		self.cname:SetTextColor(1, 0, 0)
	end
end
events.PARTY_MEMBERS_CHANGED = events.RAID_ROSTER_UPDATE

function events:UNIT_AURA(_, unit)
	if unit == self.unit then
		UpdateAuraInfo(self)
	end
end
events.UNIT_AURASTATE = events.UNIT_AURA

function events:PLAYER_REGEN_DISABLED()
	if self.char_name then
		self.has_aura = nil
		self.recording = true
		self.gain = 0
		self.elapsed = 0
		self.elapsed_with = 0
		if self.unit then UpdateAuraInfo(self) end
		self:SetScript("OnUpdate", self.OnUpdate or Frame_OnUpdate)
	end
end

function events:PLAYER_REGEN_ENABLED()
	self:SetScript("OnUpdate", nil)
	self.sb:SetStatusBarColor(0.905, 0.678, 0.168)
	self.app:SetText("")
	self.recording = nil
end

function events:UNIT_TARGET(_, unit)
	if unit == self.targetting then
		return self:RAID_ROSTER_UPDATE()
	end
end

function events:PLAYER_TARGET_CHANGED()
	return self:UNIT_TARGET(nil, "player")
end
events.PLAYER_FOCUS_CHANGED = events.PLAYER_TARGET_CHANGED

local parent = CreateFrame("Frame", nil, UIParent)
parent:SetToplevel(true)
parent:SetMovable(true)
parent:EnableMouse(true)
parent:RegisterForDrag("LeftButton")
parent:SetScript("OnDragStart", function (self)
	self:StartMoving()
end)
parent:SetScript("OnDragStop", function (self)
	self:StopMovingOrSizing()
	UptimeDB.anchorX = self:GetLeft()
	UptimeDB.anchorY = self:GetBottom()
end)
parent:SetBackdrop({
	bgFile = "Interface\\TutorialFrame\\TutorialFrameBackground",
	edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
	tile = true,
	edgeSize = 16,
	tileSize = 32,
	insets = {
		left = 5,
		right = 5,
		top = 5,
		bottom = 5
	}
})

local function BaseFrame_OnEvent(self, event, ...)
	local handler = self[event]
	if handler then
		return handler(self, event, ...)
	end
end

parent:SetScript("OnEvent", BaseFrame_OnEvent)

parent:RegisterEvent"ADDON_LOADED"
function parent:ADDON_LOADED(event, name)
	if name == "uptime" then
		self:UnregisterEvent"ADDON_LOADED"
		self.ADDON_LOADED = nil
		self:ClearAllPoints()
		if UptimeDB.anchorX then
			self:SetPoint("BOTTOMLEFT", UptimeDB.anchorX, UptimeDB.anchorY)
		else
			self:SetPoint("CENTER")
		end
		parent:SetWidth(UptimeDB.width or 128 + 4)
		if not UptimeDB.settings then
			UptimeDB.settings = {
				{ "Radnarok", 2565, },
				{ "Radnarok", 33763, },
				{ "Khaldyr", 5171, },
				{ "Jharheck", 5171, },
				{ "Jarek", 2565, },
				{ "Arilyn", "Vitesse de l'éclair", },
				{ "Saccageur du Vide", "Lucioles (farouche)", true, },
				{ "Thanator", 27179, },
--~ 				{ "Thanator", 20134, },
			}
		end
		if not UptimeDB.selections then
			UptimeDB.selections = {
				["Jerryak - Dalaran"] = { 8, 9, },
				["Jharheck - Dalaran"] = { 4, },
				["Jarek - Dalaran"] = { 5, },
			}
		end
		local selection = UptimeDB.selections[UnitName("player").." - "..GetRealmName()]
		if selection then
			for p, i in ipairs(selection) do
				local entry = UptimeDB.settings[i]
				if not entry then
					table.remove(selection, p)
				else
					CreateUpTimeFrame(unpack(entry))
				end
			end
		end
	end
end

local function MenuHandler(index)
	local entry = UptimeDB.settings[index]
	if entry then
		CreateUpTimeFrame(unpack(entry))
	end
end

function parent:MakeMenu()
	local menu = {}
	local index = 1
	while index < #UptimeDB.settings do
		local entry = UptimeDB.settings[index]
		local name, spell, debuff = unpack(entry)
		local unit = GetUnitId(name)
		local color = unit and RAID_CLASS_COLORS[select(2, UnitClass(unit))] or GRAY_FONT_COLOR
		if type(spell) == "number" then spell = GetSpellInfo(spell) end
		if type(spell) == "string" then
			local name = name.." "..(debuff and RED_FONT_COLOR_CODE or LIGHTYELLOW_FONT_COLOR_CODE)..spell..FONT_COLOR_CODE_CLOSE
			menu[#menu + 1] = {
				text = name,
				func = MenuHandler,
				arg1 = index,
				textR = color.r,
				textG = color.g,
				textB = color.b,
			}
			index = index + 1
		else
			table.remove(UptimeDB.settings, index)
		end
	end
	
	return menu
end

function parent:OpenMenu()
	local dropdown = self.dropdown
	if not dropdown then
		dropdown = CreateFrame("Frame", "UptimeDropDown", nil, "UIDropDownMenuTemplate")
		dropdown.point = "TOPLEFT"
		dropdown.relativePoint = "TOPRIGHT"
		dropdown.displayMode = "MENU"
		dropdown.relativeTo = self
		self.dropdown = dropdown
	end
	EasyMenu(self:MakeMenu(frame), dropdown)
end

do
	local close = CreateFrame("Button", nil, parent)

	local makeTexture = function (path, blend)
		local t = close:CreateTexture()
		t:SetAllPoints(close)
		t:SetTexture(path)
		if blend then
			t:SetBlendMode(blend)
		end
		return t
	end

	close:SetNormalTexture(makeTexture("Interface\\Buttons\\UI-Panel-MinimizeButton-Up"))
	close:SetPushedTexture(makeTexture("Interface\\Buttons\\UI-Panel-MinimizeButton-Down"))
	close:SetHighlightTexture(makeTexture("Interface\\Buttons\\UI-Panel-MinimizeButton-Highlight", "ADD"))
	close:RegisterForClicks("LeftButtonUp", "RightButtonUp")
	close:SetScript("OnClick", function (self, button)
		if button == "LeftButton" then
			parent:Hide()
		else
			parent:OpenMenu()
		end
	end)
	close:SetScript("OnEnter", function (self) self:SetAlpha(1) end)
	close:SetScript("OnLeave", function (self) self:SetAlpha(.2) end)

	close:SetWidth(32)
	close:SetHeight(32)
	close:SetAlpha(.2)
	close:ClearAllPoints()
	close:SetPoint("TOPRIGHT", 1, 0)
end

local function Frame_OnShow(self)
	self:SetScript("OnEvent", BaseFrame_OnEvent)
end
local function Frame_OnHide(self)
	self:SetScript("OnEvent", nil)
end

local function Frame_OnClick(self, button)
	if button == "LeftButton" then
		if not ChatFrameEditBox:IsVisible() or not self.elapsed then return end
		ChatFrameEditBox:Insert(CHAT_TEXT:format(
			self.aura_name, self.char_name, self.gain, self.elapsed, self.elapsed_with / self.elapsed * 100))
	else
		SetFrameInfo(self)
	end
end

local function CreateBaseFrame()
	local f = CreateFrame("Button", nil, parent)
	f:SetWidth(UptimeDB.width or 128)
	f:SetHeight(UptimeDB.height or 32)
	f:EnableMouse(true)
	f:RegisterForClicks("LeftButtonUp", "RightButtonUp")
	f:SetScript("OnClick", Frame_OnClick)
	f:SetScript("OnShow", Frame_OnShow)
	f:SetScript("OnHide", Frame_OnHide)
	for event, handler in pairs(events) do
		f[event] = handler
		f:RegisterEvent(event)
	end

	local sb = CreateFrame("StatusBar", nil, f)
	f.sb = sb
	sb:SetPoint("TOPLEFT", 6, -6)
	sb:SetWidth(f:GetWidth() - 12)
	sb:SetHeight(f:GetHeight() - 12)
	sb:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
	sb:SetStatusBarColor(0.905, 0.678, 0.168)
	sb:SetMinMaxValues(0, 100)
	sb:SetValue(0)

	local cname = sb:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
	f.cname = cname
	cname:SetPoint("TOPLEFT", -6, 6)
	cname:SetWidth(f:GetWidth() / 2)
	cname:SetHeight(f:GetHeight() / 2)

	local aname = sb:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
	f.aname = aname
	aname:SetPoint("TOPRIGHT", 6, 6)
	aname:SetWidth(f:GetWidth() / 2)
	aname:SetHeight(f:GetHeight() / 2)

	local text = sb:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
	f.text = text
	text:SetPoint("TOPLEFT", -6, - f:GetHeight() / 2 + 8)
	text:SetWidth(f:GetWidth())
	text:SetHeight(f:GetHeight() / 2)

	local app = sb:CreateFontString(nil, "OVERLAY", "GameFontNormal")
	f.app = app
	app:SetAllPoints()

	f:Hide()
	return f
end

local frames = {}

local function GetUpTimeFrame(char_name, aura_name, is_debuff)
	for _, frame in ipairs(frames) do
		if not frame.char_name then
			SetFrameInfo(frame, char_name, aura_name, is_debuff)
			return frame
		end
	end
	local frame = CreateBaseFrame()
	frames[#frames + 1] = frame
	SetFrameInfo(frame, char_name, aura_name, is_debuff)
	return frame
end

local function GetEntryIndex(char_name, aura_name, is_debuff, add)
	local settings = UptimeDB.settings
	for i, entry in ipairs(settings) do
		if entry[1] == char_name and entry[2] == aura_name and entry[3] == is_debuff then
			return i
		end
	end
	if add then
		local index = #settings + 1
		settings[index] = {char_name, aura_name, is_debuff}
		parent.menu = nil
		return index
	end
end

local function CreateUpTimeFrame(char_name, aura_name, is_debuff)
	local frame = GetUpTimeFrame(char_name, aura_name, is_debuff)
	ReorderUpTimeFrames()

	local index = GetEntryIndex(char_name, aura_name, is_debuff, true)
	frame.entry_index = index
	local v = UnitName("player").." - "..GetRealmName()
	local selection = UptimeDB.selections[v]
	if not selection then
		selection = {}
		UptimeDB.selections[v] = selection
	end
	local found
	for i, p in ipairs(selection) do
		if p == index then
			if i ~= 1 then
				table.remove(selection, i)
				table.insert(selection, 1, index)
			end
			found = true
			break
		end
	end
	if not found then
		selection[#selection + 1] = index
	end
	return frame
end

function ReorderUpTimeFrames()
	local h = 14
	local prev
	for _, frame in ipairs(frames) do
		if frame.char_name then
			if not prev then
				frame:SetPoint("TOP", 0, -12)
			else
				frame:SetPoint("TOP", prev, "BOTTOM")
			end
			h = h + frame:GetHeight()
			prev = frame
		end
	end
	if h <= 0 then
		parent:Hide()
	else
		parent:Show()
		parent:SetHeight(h)
	end
end

function SetFrameInfo(frame, char_name, aura_name, is_debuff)
	if char_name then
		frame.char_name = char_name
		frame.cname:SetText(char_name)
		if type(aura_name) == "number" then
			aura_name = GetSpellInfo(aura_name)
		end
		assert(aura_name)
		frame.aura_name = aura_name
		frame.aname:SetText(aura_name)
		frame.getaura = is_debuff and UnitDebuff or UnitBuff
		frame:RAID_ROSTER_UPDATE()
		if InCombatLockdown() then
			frame:PLAYER_REGEN_DISABLED()
		end
		frame:Show()
	else
		local index = frame.entry_index
		local v = UnitName("player").." - "..GetRealmName()
		local selection = UptimeDB.selections[v]
		if selection then
			for i, p in ipairs(selection) do
				if p == index then
					table.remove(selection, i)
					break
				end
			end
		end
		frame.entry_index = nil
		frame.char_name = nil
		frame.aura_name = nil
		frame.getaura = nil
		frame.has_aura = nil
		frame:Hide()
		ReorderUpTimeFrames()
	end
end

_G.CreateUpTimeFrame = CreateUpTimeFrame

SlashCmdList.UPTIME = function (input)
	local cmd, param = string.split(" ", input:trim(), 2)
	cmd = cmd:lower():trim()
	if cmd == "add" then
		local name, spell, debuff = string.split(",", param)
		name = name:trim()
		spell = spell:trim()
		debuff = debuff and debuff:trim()
		spell = tonumber(spell) or spell
		CreateUpTimeFrame(name, spell, debuff)
	end
end
SLASH_UPTIME1="/uptime"
